<?php
require_rewrite(SYSTEM_PATH . 'config/mysql_config.php');
require_rewrite(SYSTEM_PATH . 'lib/DB_Exception.php');

/**
 * MySQLi数据访问封装类
 */
class DB_MySQLi{
    /**
     * 静态变量，指向唯一的数据库实例
     * @var <object>
     */
    private static $instance = null;

    /**
     * 读写主库连接
     * @var <object>
     */
    private $connection_write = null;

    /**
     * 只读库连接
     * @var <object>
     */
    private $connection_readonly = null;

    /**
     * 当前数据库连接
     * @var <object>
     */
    private $connection = null;

    /**
     * 连接模式，1-同时连接主库和只读库；2-只连接主库；3-只连接只读库
     * @var <type> 
     */
    private $mode = 1;

    /**
     * 内部结果集 外部不允许直接进行操作
     * @var <resource>
     */
    protected $res = null;

    /**
     * 记录对象最后一次执行的sql语句
     * @var <string>
     */
    protected $sql = null;

    /**
     * 构造函数
     * @param <int> $mode 模式，1-同时连接主库和只读库；2-只连接主库；3-只连接只读库
     */
    private function __construct($mode) {
        //判断mysqli扩展是否已加载
        if ( !extension_loaded('mysqli') ) {
            throw new DB_Exception('mysqli is not supported!');
        }
        //设置类属性
        $this->mode = $mode;

        //根据连接模式连接不同的数据库
        switch ($mode) {
            case 1:
                $this -> initMaster();
                $this -> initSlave();
                //根据模式将当前连接设为主库连接，像real_escape_string这些函数会根据连接的属性来执行
                $this -> useMaster();
                break;
            case 2:
                $this -> initMaster();
                $this -> useMaster();
                break;
            case 3:
                $this -> initSlave();
                $this ->useSlave();
                break;
            default:
                break;
        }
    }

    /**
     * 析构函数
     */
    public function __destruct() {
        //释放内存中的结果集
        $this->freeResult();
        //关闭连接
        $this->closeConnection();
        //释放内嵌对象
        unset($this->connection);
        unset($this->connection_readonly);
        unset($this->connection_write);
    }

    /**
     * 静态方法，返回数据库类 初始化设定是否抛出异常
     * @param <int> $mode 模式，1-同时连接主库和只读库；2-只连接主库；3-只连接只读库
     * @return <object> 数据库连接对象
     */
    public static function getInstance($mode = 1) {
        if(self :: $instance == null) {
            self :: $instance = new db_mysqli($mode);
        }
        return self :: $instance;
    }

    /**
     * 创建数据库连接
     * @param <array> $config 数据库配置，数组类型
     * @return <object> 数据库连接对象
     */
    private function initConn($config){
        if( !empty($config) ){
            $con = new mysqli($config['host'], $config['username'], $config['password'], $config['dbname']);
            //如果连接失败，抛出异常
            if( mysqli_connect_error() ) {
                throw new DB_Exception( 'Connect Error (' . mysqli_connect_errno() . ') : ' . mysqli_connect_error() );
            }
            return $con;
        }
        throw new DB_Exception('Connect to DB with empty config.');
    }

    /**
     * 初始化读写主库
     */
    private function initMaster(){
        $this -> connection_write = $this->initConn(DBConfig::$write);
        $this -> connection_write -> set_charset('utf8');
    }

    /**
     * 初始化只读从库
     */
    private function initSlave(){
        $this -> connection_readonly = $this->initConn(DBConfig::$readonly);
        $this -> connection_readonly -> set_charset('utf8');
    }

    /**
     * 使用读写主库
     */
    private function useMaster(){
        if( $this -> connection_write == null){
            throw new DB_Exception('The master database is not availible when switch to master!');
        }
        $this -> connection = $this -> connection_write;
    }

    /**
     * 使用只读从库
     */
    private function useSlave(){
        if( $this -> connection_readonly == null){
            throw new DB_Exception('The slave database is not availible when switch to slave!');
        }
        $this -> connection = $this -> connection_readonly;
    }

    /**
     * 根据SQL语句切换主从数据库
     */
    private function switchConnection($query){
        $query = trim($query);
        $isSelect = strtoupper( substr($query, 0, 6) ) == 'SELECT';
        $isSelect ? $this->useSlave() : $this->useMaster();
    }

    /**
     * 检测当前连接是否有效
     * @return boolean 有效返回True，否则返回False。
     */
    public function isConnected(){
        return ( (bool) ($this->connection instanceof mysqli) );
    }

    /**
     * 关闭连接
     * @return void
     */
    public function closeConnection(){
        if ($this->isConnected()) {
            if( !$this->connection->close() ){
                throw new DB_Exception('Close connection error!');
            }
        }
        $this->connection = null;
    }

    /**
     * 对影响SQL执行的特殊字符进行转义
     * @param mixed $value 要处理的字符串
     * @return string  转义后的字符串
     */
    public function escapeString($value) {
        if( is_int($value) || is_float($value) ) {
            return $value;
        }
        return $this->connection->real_escape_string($value);
    }
    
    /**
     * 查询
     * @param <string> $query SQL语句
     * @return <mixed> 失败时返回FALSE。成功时 SELECT, SHOW, DESCRIBE 和 EXPLAIN 语句将会返回一个对象，其他语句返回TRUE。
     */
    public function query($query){
        if( empty($query) || trim($query) == '' ){
            return null;
        }
        $query = $this->sql = trim($query);
        //当连接模式为主从分开的时候，需要根据SQL语句来切换数据库
        if($this->mode == 1){
            $this->switchConnection($query);
        }
        //执行SQL并返回MySQLi_Result对象
        $this->res = $this->connection->query($query);
        //如果执行报错抛出异常
        if($this->res === false){
            $msg = $this->getErrorInfo();
            throw new DB_Exception($msg, $code);
        }
        return $this->res;
    }

    /**
     * 返回最后一次操作产生的自增ID
     * @return <int> 最后一次操作产生的自增ID
     */
    public function getlastInsertId(){
        return $this->connection->insert_id;
    }

    /**
     * 获取最近一次操作影响的行数
     * @return <int> 收影响的行数
     */
    public function getEffectRows(){
        return $this->connection->affected_rows;
    }

    /**
     * 获取最近一次查询返回的结果集所包含的字段列数
     * @return <int> 结果集包含的字段列数
     */
    public function getColumns(){
        return $this->connection_readonly->field_count;
    }

    /**
     * 获取最近一次SQL执行后的统计信息，支持Insert、Update、Alter、Load Data
     * 对Select无效，Select建议采用getEffectRows()
     * 示例: Records: 100 Duplicates: 0 Warnings: 0
     * @return <string> 统计信息
     */
    public function getQueryInfo(){
        return $this->connection->info;
    }

    /**
     * 获取记录集的行数
     * @param <string> $query SQL语句
     * @return <int> 记录集的行数
     */
    public function getRowCount($query = ''){
        if( !empty($query) ){
            $this->query($query);
        }
        return empty ($this->res) ? null : $this->res->num_rows;
    }

    /**
     * 获取最近一次操作的系统错误信息
     * 可通过MySQL帮助文档查找错误代码对应的错误信息
     * @return <string> 错误信息，已经错误发生时的数据
     */
    public function getErrorInfo(){
        //获取最近一次操作的SQLSTATE 错误信息，5位的错误信息代码，HY000代表与MySQL错误代码匹配的错误。
        $state = $this->connection->sqlstate;
        $state_code = ($state == '00000') ? 'no error' : $state;
        $error = 'error_code: ' . $this->connection->errno . '; msg: ' .  $this->connection->error . '; mysql_state: ' . $state_code . '; mode = ' . $this->mode . '; sql = ' . $this->sql;
        return $error;
    }

    /**
     * 将结果集上的指针定位到指定的位置，便于获取指定位置上的数据。
     * 定位后通过fetchRow_xxx()函数获取数据。
     * @param <stirng> $query SQL查询语句，如果为空则将现有的结果集作为检索对象
     * @param <int> $index 数字下标，从0开始计算
     * @return <bool> 检索成功返回true，否则返回false。
     */
    public function dataSeek($query = '', $index = -1){
        //校验搜索的索引是否合法
        if($index < 0){
            return null;
        }
        //如果SQL语句存在，则先执行SQL，否则使用当前结果集中的数据
        if( !empty($query) ){
             $this->query($query);
        }
        $result = $this->res;

        //如果结果集为空则直接返回NULL
        if( empty($result) ){
            return null;
        }
        //如果索引值超出数据量范围，直接返回null
        if( $result->num_rows <= ($index + 1) ){
            return null;
        }
        //返回数据，如果结果集为空，返回null
        return $result->data_seek($index);
    }

    /**
     * 根据查询语句获取所有数据，以数组的形式返回，如果无数据或者出错返回NULL。
     * @param <string> $query SQL语句
     * @param <constant> $resultType 数组的类型， 可选的值有: MYSQLI_ASSOC, MYSQLI_NUM, or MYSQLI_BOTH.
     * @return <mixed> 对应的数据
     */
    protected function getData($query, $resultType = MYSQLI_ASSOC){
        $data = null;
        $result = $this->query($query);

        if( !empty($result) ){
            $data = array();
            while( $row = $result->fetch_array($resultType) ){
                $data[] = $row;
            }
        }
        return $data;
    }

    /**
     * 根据查询语句获取所有数据，以数组的形式返回，数组中每行数据的下标为字段名，如果无数据或者出错返回NULL。
     * @param <type> $query SQL语句
     * @return <type>
     */
    public function getData_ASSOCS($query){
        return $this->getData($query, MYSQLI_ASSOC);
    }

    /**
     * 根据查询语句获取所有数据，以数组的形式返回，数组中每行数据的下标为数字，如果无数据或者出错返回NULL。
     * @param <type> $query SQL语句
     * @return <type>
     */
    public function getData_NUM($query){
        return $this->getData($query, MYSQLI_NUM);
    }

    /**
     * 根据查询语句获取所有数据，以数组的形式返回，数组中每行数据的下标为字段名或者数字，如果无数据或者出错返回NULL。
     * @param <type> $query SQL语句
     * @return <type> 
     */
    public function getData_BOTH($query){
        return $this->getData($query, MYSQLI_BOTH);
    }

    /**
     * 已数组的形式获取一行数据，数组的索引下标为字段名
     * @return <array> 对应的数据
     */
    public function fetchRow_ASSOCS(){
        return empty ($this->res) ? null : $this->res->fetch_assoc();
    }

    /**
     * 已数组的形式获取一行数据，数组的索引下标为0开始的数字
     * @return <array> 对应的数据
     */
    public function fetchRow_NUM(){
        return empty ($this->res) ? null : $this->res->fetch_row();
    }

    /**
     * 已数组的形式获取一行数据，数组的索引下标同时支持字段名和者数字
     * @return <array> 对应的数据
     */
    public function fetchRow_BOTH(){
        return empty ($this->res) ? null : $this->res->fetch_array(MYSQLI_BOTH);
    }

    /**
     * 释放内存中存储结果集的空间
     */
    public function freeResult(){
    //如果res不为空且类型是 mysqli_result 对象时才执行free函数
    if( !empty($this->res) && ($this->res instanceof mysqli_result) ){
            $this->res->free();
        }
    }

    /*
    public function multiQuery($query){
        if( empty($query) || trim($query) == '' ){
            return null;
        }
        $query = $this->sql = trim($query);

    }
     */
    
    /**
     *  开始事务，只支持主库操作，表必须是InnoDB类型
     */
    public function beginTransaction(){
        $this->useMaster();
        $this->connection->autocommit(false);
    }

    /**
     * 提交事务，只支持主库操作，表必须是InnoDB类型
     */
    public function commit(){
        $this->useMaster();
        $this->connection->commit();
        $this->connection->autocommit(true);
    }

    /**
     * 回滚事务，只支持主库操作
     */
    public function rollBack(){
        $this->useMaster();
        $this->connection->rollback();
        $this->connection->autocommit(true);
    }
}
?>